// SPDX-FileCopyrightText: 2024 Adam Evyčędo
//
// SPDX-License-Identifier: MPL-2.0

package gott

import (
	"context"
	"fmt"
	"log"
	"log/slog"
	"reflect"
	"runtime"
)

type LogLevel int

// LogLevel specifies what to log:
// Quiet logs nothing,
// Error logs only errors,
// Debug logs functions that run,
// Info logs run and skipped functions
const (
	Quiet LogLevel = iota
	Error
	Debug
	Info
)

func fnName(fn interface{}) string {
	return runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()
}

func logErr(e error, fn interface{}, l *slog.Logger) {
	if l != nil {
		l.LogAttrs(context.Background(), slog.LevelError, "Function returned error", slog.Attr{Key: "function", Value: slog.StringValue(fnName(fn))}, slog.Attr{Key: "error", Value: slog.StringValue(e.Error())})
	} else {
		log.Printf("Function %s returned error: %v\n", fnName(fn), e)
	}
}

func logMsg(msg string, fn interface{}, l *slog.Logger, level slog.Level) {
	if l != nil {
		l.LogAttrs(context.Background(), level, msg, slog.Attr{Key: "function", Value: slog.StringValue(fnName(fn))})
	} else {
		log.Printf("%s: %s\n", msg, fnName(fn))
	}
}

// Exception is a type encapsulating anything contained in panic.
// It implements Error() and therefore can be used as error.
type Exception struct {
	E interface{}
}

func (e Exception) Error() string {
	return fmt.Sprintf("function panicked with %v", e.E)
}

// R is a simplification of Either monad. It’s either succesful—when its error
// is nil—or unsuccessful otherwise.
type R[T any] struct {
	S        T
	E        error
	LogLevel LogLevel
	Logger   *slog.Logger
}

// Bind performs f on the receiver’s success value and assigns the returned
// value and error to the receiver if it’s is successful. In either case, Bind
// returns the receiver.
// Bind operates on functions that return value and error.
func (r R[T]) Bind(f func(T) (T, error)) R[T] {
	if r.E == nil {
		if r.LogLevel >= Debug || r.Logger != nil {
			logMsg("running", f, r.Logger, slog.LevelDebug)
		}
		r.S, r.E = f(r.S)
		if r.E != nil {
			if r.LogLevel >= Error || r.Logger != nil {
				logErr(r.E, f, r.Logger)
			}
			r.E = fmt.Errorf("while running %s: %w", fnName(f), r.E)
		}
	} else {
		if r.LogLevel >= Info || r.Logger != nil {
			logMsg("skipping", f, r.Logger, slog.LevelInfo)
		}
	}
	return r
}

// Map performs f on the receiver’s success value and assigns the returned
// value to the receiver if it’s is successful. In either case, Map returns the
// receiver.
// Map operates on functions that are always successful and return only one
// value.
func (r R[T]) Map(f func(T) T) R[T] {
	if r.E == nil {
		if r.LogLevel >= Debug || r.Logger != nil {
			logMsg("running", f, r.Logger, slog.LevelDebug)
		}
		r.S = f(r.S)
	} else {
		if r.LogLevel >= Info || r.Logger != nil {
			logMsg("skipping", f, r.Logger, slog.LevelInfo)
		}
	}
	return r
}

// Tee performs f on the receiver’s success value and assigns the returned
// error to the receiver if it’s is successful. In either case, Tee returns the
// receiver.
// Tee operates on functions that only perform side effects and might return an
// error
func (r R[T]) Tee(f func(T) error) R[T] {
	if r.E == nil {
		if r.LogLevel >= Debug || r.Logger != nil {
			logMsg("running", f, r.Logger, slog.LevelDebug)
		}
		r.E = f(r.S)
		if r.E != nil {
			if r.LogLevel >= Error || r.Logger != nil {
				logErr(r.E, f, r.Logger)
			}
			r.E = fmt.Errorf("while running %s: %w", fnName(f), r.E)
		}
	} else {
		if r.LogLevel >= Info || r.Logger != nil {
			logMsg("skipping", f, r.Logger, slog.LevelInfo)
		}
	}
	return r
}

// SafeTee performs f on the receiver’s success value if the receiver is
// successful. In either case, SafeTee returns the receiver.
// SafeTee operates on functions that only perform side effects and are always
// successful.
func (r R[T]) SafeTee(f func(T)) R[T] {
	if r.E == nil {
		if r.LogLevel >= Debug || r.Logger != nil {
			logMsg("running", f, r.Logger, slog.LevelDebug)
		}
		f(r.S)
	} else {
		if r.LogLevel >= Info || r.Logger != nil {
			logMsg("skipping", f, r.Logger, slog.LevelInfo)
		}
	}
	return r
}

// Catch performs f on the receiver’s success value and assigns the returned
// vale to the receiver if it’s successful. If f panics, Catch recovers and
// stores the value passed to panic in receiver’s error as Exception. In either
// case, Catch returns the receiver.
func (r R[T]) Catch(f func(T) T) (r2 R[T]) {
	r2 = r
	if r2.E == nil {
		if r2.LogLevel >= Debug || r.Logger != nil {
			logMsg("running", f, r.Logger, slog.LevelDebug)
		}
		defer func() {
			if err := recover(); err != nil {
				if r2.LogLevel >= Error || r2.Logger != nil {
					logErr(Exception{err}, f, r.Logger)
				}
				r2.E = fmt.Errorf("while running %s: %w", fnName(f), Exception{err})
			}
		}()
		r2.S = f(r.S)
	} else {
		if r.LogLevel >= Info || r.Logger != nil {
			logMsg("skipping", f, r.Logger, slog.LevelInfo)
		}
	}
	return
}

// Revover tries to put processing back on the happy track.
// If receiver is not successful, Recover calls the passed function and
// assignes the returned value and error to the receiver. In either case,
// Recover returns the receiver.
func (r R[T]) Recover(f func(T, error) (T, error)) R[T] {
	if r.E != nil {
		if r.LogLevel >= Debug || r.Logger != nil {
			logMsg("running", f, r.Logger, slog.LevelDebug)
		}
		r.S, r.E = f(r.S, r.E)
		if r.E != nil {
			if r.LogLevel >= Error || r.Logger != nil {
				logErr(r.E, f, r.Logger)
			}
			r.E = fmt.Errorf("while running %s: %w", fnName(f), r.E)
		}
	} else {
		if r.LogLevel >= Info || r.Logger != nil {
			logMsg("skipping", f, r.Logger, slog.LevelInfo)
		}
	}
	return r
}

// Handle performs onSuccess on the receiver’s success value if the receiver is
// successful, or onError on the receiver’s error otherwise. In either case,
// Handle returns the receiver.
func (r R[T]) Handle(onSuccess func(T), onError func(error)) R[T] {
	if r.E == nil {
		if r.LogLevel >= Debug || r.Logger != nil {
			logMsg("running", onSuccess, r.Logger, slog.LevelDebug)
		}
		onSuccess(r.S)
	} else {
		if r.LogLevel >= Debug || r.Logger != nil {
			logMsg("running", onError, r.Logger, slog.LevelDebug)
		}
		onError(r.E)
	}
	return r
}
